package edu.osu.cse.c651.cryptochess;

import java.net.*;
import android.content.Intent;
import android.os.AsyncTask;
import android.util.Log;

/**
 * This class controls the sending and receiving of datagram packets for the find
 * game activity. It is meant to run in the background while the user is looking
 * at the find game activity.
 * 
 * @author Katherine Tornwall
 */
public class FindGameController extends AsyncTask<FindGameActivity, User, Void> {
	/**
	 * The address for broadcasting messages, 255.255.255.255.
	 */
	private InetAddress broadcastGroup;
	/**
	 * The local address of this client.
	 */
	private InetAddress myAddress;
	/**
	 * The packet used to broadcast that this client has joined the find game
	 * activity.
	 */
	private DatagramPacket announcePacket;
	/**
	 * The packet used to reply to announce broadcasts. The destination IP
	 * address should be set before this member variable is used.
	 */
	private DatagramPacket announceReplyPacket;
	/**
	 * The packet used to broadcast that this client has left the find game
	 * activity.
	 */
	private DatagramPacket removePacket;
	/**
	 * The local user.
	 */
	private User user;
	/**
	 * Set to the opponent player when an opponent has been found.
	 */
	private User opponent;
	/**
	 * The value of the request that has yet to be processed. Null if there is
	 * no pending transaction.
	 */
	private Request transReq;
	/**
	 * The instance of the find game activity that is using this controller.
	 */
	private static FindGameActivity ui;
	/**
	 * A static instance of this controller that is passed to find game activity.
	 */
	private static FindGameController classInstance;

	/**
	 * The UUID used to identify messages as belonging to the cryptochess game
	 * client.
	 */
	final static private String CRYPTO_CHESS_UUID = "{58e9b2b7-b74d-4ccb-ba76-89f757a32472}";
	/**
	 * The port all cryptochess find game clients use to listen to UDP messages.
	 */
	final static private int LISTEN_PORT = 19725;

	/**
	 * Initializes the data for the find game controller.
	 */
	private FindGameController() {
		transReq = null;
		user = null;
		try {
			broadcastGroup = InetAddress.getByName("255.255.255.255");
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Sets the corresponding FindGameActivity instance for this controller.
	 * @param activity the FindGameActivity associated with this controller.
	 */
	public static void setActivity(FindGameActivity activity) {
		ui = activity;
	}

	/**
	 * Returns the static instance of this controller.
	 * @return the instance of this class.
	 */
	public static FindGameController getInstance() {
		if (classInstance == null) {
			classInstance = new FindGameController();
		}

		return classInstance;
	}

	/**
	 * Sets the local address for this client.
	 * @param s this client's address
	 */
	public void setAddress(String s) {
		try {
			myAddress = InetAddress.getByName(s);
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Sets the local user for this client. This must be called before using the
	 * controller as it determines how the packets to be sent are formed.
	 * 
	 * @param u the local user
	 */
	public void setUser(User u) {
		user = u;
		byte[] announceBuf = (CRYPTO_CHESS_UUID + ";"
				+ Request.announce.ordinal() + ";" + this.user.toString() + ";")
				.getBytes();
		announcePacket = new DatagramPacket(announceBuf, 0, announceBuf.length,
				broadcastGroup, LISTEN_PORT);
		byte[] announceReplyBuf = (CRYPTO_CHESS_UUID + ";"
				+ Request.announceReply.ordinal() + ";" + this.user.toString() + ";")
				.getBytes();
		announceReplyPacket = new DatagramPacket(announceReplyBuf, 0,
				announceReplyBuf.length, broadcastGroup, LISTEN_PORT);
		byte[] removeBuf = (CRYPTO_CHESS_UUID + ";" + Request.leave.ordinal()
				+ ";" + this.user.toString() + ";").getBytes();
		removePacket = new DatagramPacket(removeBuf, 0, removeBuf.length,
				broadcastGroup, LISTEN_PORT);
	}

	/**
	 * Broadcasts that this client is an active user in the find game activity.
	 */
	public void broadcastAvailable() {
		DatagramSocket broadcastSocket = null;
		try {
			broadcastSocket = new DatagramSocket();
			broadcastSocket.setBroadcast(true);
			broadcastSocket.send(announcePacket);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (broadcastSocket != null)
				broadcastSocket.close();
		}
	}

	/**
	 * Broadcasts that this client is no longer an active user in the find game activity.
	 */
	public void broadcastRemove() {
		DatagramSocket broadcastSocket = null;
		try {
			broadcastSocket = new DatagramSocket();
			broadcastSocket.setBroadcast(true);
			broadcastSocket.send(removePacket);
			broadcastSocket.close();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (broadcastSocket != null)
				broadcastSocket.close();
		}
	}

	/**
	 * Updates the user interface with the information that was received from the last
	 * datagram packet. <br/><br />
	 * 
	 * {@inheritDoc}
	 * 
	 * @param u the user that was included in the packet just received by the controller
	 * @author Katherine Tornwall
	 */
	@Override
	public void onProgressUpdate(User... u) {
		if (transReq != null && u[0] != null) {
			switch (transReq) {
			case accept:
				Intent intent = new Intent(ui, GameActivity.class);
				intent.putExtra("user", user);
				intent.putExtra("opponent", opponent);
				intent.putExtra("color", user.getColor());
				intent.putExtra("ip", opponent.getIpAddress());
				ui.startActivity(intent);
				break;
			case announce:
			case announceReply:
				ui.addUser(u[0]);
				break;
			case leave:
				ui.removeUser(u[0]);
				break;
			case play: // Show the user an accept dialog
				ui.gameRequestNotification(u[0]);
				break;
			case reject:
				break;
			}

			transReq = null;
		}
	}

	/**
	 * Receives datagram packets for the client and then passes them off to be parsed.
	 * 
	 * {@inheritDoc}
	 * 
	 * @author Katherine Tornwall
	 */
	@Override
	protected Void doInBackground(FindGameActivity... act) {
		DatagramSocket listeningSocket = null;
		ui = act[0];
		try {
			byte[] data = new byte[100];
			DatagramPacket receivedBlock = new DatagramPacket(data, 100);
			listeningSocket = new DatagramSocket(LISTEN_PORT);
			listeningSocket.setBroadcast(true);
			while (opponent == null) {
				listeningSocket.receive(receivedBlock);
				if (!receivedBlock.getAddress().equals(myAddress))
					parsePacket(receivedBlock);
				if(transReq == Request.accept) { // leave if we got a accept request
					listeningSocket.close();
					listeningSocket = null;
					return null;
				}
					
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (listeningSocket != null)
				listeningSocket.close();
		}
		return null;
	}

	/**
	 * Parses the packets that come in to determine the transaction type and parses out
	 * the user from the packet. This function also checks the UUID to make sure that
	 * the packet came from a cryptochess client.
	 * 
	 * @param p the packet just received by the controller
	 * @author Katherine Tornwall
	 */
	private void parsePacket(DatagramPacket p) {
		byte[] buf = p.getData();
		String[] arguments = null;
		if (buf != null) {
			String str = new String(buf);
			arguments = str.split(";");
		}

		if (arguments != null && arguments.length == 4) {
			if (!arguments[0].equals(CRYPTO_CHESS_UUID)) {
				return; // Not our game...
			}

			if (Integer.parseInt(arguments[1]) == Request.announceReply
					.ordinal()) {
				Log.i("CryptoChess",
						"received Announce Reply " + p.getAddress() + "::"
								+ p.getPort());
				transReq = Request.announceReply;
			} else if (Integer.parseInt(arguments[1]) == Request.announce
					.ordinal()) {
				Log.i("CryptoChess", "received Announce " + p.getAddress()
						+ "::" + p.getPort());
				transReq = Request.announce;
				performAnnounce(p);
			} else if (Integer.parseInt(arguments[1]) == Request.leave
					.ordinal()) {
				Log.i("CryptoChess", "received Leave " + p.getAddress() + "::"
						+ p.getPort());
				transReq = Request.leave;
			} else if (Integer.parseInt(arguments[1]) == Request.play.ordinal()) {
				Log.i("CryptoChess", "received Play " + p.getAddress() + "::"
						+ p.getPort());
				transReq = Request.play;
			} else if (Integer.parseInt(arguments[1]) == Request.reject
					.ordinal()) {
				Log.i("CryptoChess", "received Reject " + p.getAddress() + "::"
						+ p.getPort());
				transReq = Request.reject;
			} else {
				return; // Invalid request type
			}
			User u = User.parseUser(arguments[2]);
			if (transReq == Request.accept) {
				opponent = u;
			}
			u.setIpAddress(p.getAddress());
			publishProgress(u);
		} else if (arguments != null && arguments.length == 5) {
			if (!arguments[0].equals(CRYPTO_CHESS_UUID)) {
				return; // Not our game...
			}
			
			if (Integer.parseInt(arguments[1]) == Request.accept.ordinal()) {
				Log.i("CryptoChess", "received Accpet " + p.getAddress() + "::"
						+ p.getPort());
				transReq = Request.accept;
			}
			User u = User.parseUser(arguments[2]);
			if (transReq == Request.accept) {
				opponent = u;
			}
			user.setColor(Color.values()[Integer.parseInt(arguments[3])]);
			u.setIpAddress(p.getAddress());
			publishProgress(u);
		}
	}

	/**
	 * Performs actions after receiving an announce packet. This sends an announce reply
	 * back to the user that just performed the announce.
	 * 
	 * @param p the packet just received by the controller
	 */
	private void performAnnounce(DatagramPacket p) {
		DatagramSocket socket = null;
		try {
			InetAddress addr = p.getAddress();
			socket = new DatagramSocket();
			announceReplyPacket.setAddress(addr);
			socket.send(announceReplyPacket);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (socket != null)
				socket.close();
		}
	}

}
